const fs = require('fs-extra');
const path = require('path');
const compressing = require('compressing');
const moment = require('moment');

const Docx = require('./../adapter/docx');
const ParentReporter = require('./../default-report');
const ProjectVersionSpliter = require('./../../project-version-spliter');
const files = require('./../../../files');
const funcs = require('./../../../functions');
const fileElements = require('./../../../file-elements');
const projectVersions = require('./../../../project-versions');
const utility = require('../../../utility');
const File = require('./../../../mongo/file');
const Func = require('./../../../mongo/function');

class DefaultReporter extends ParentReporter {
  constructor(versionId, locale, version = null, project = null, creator = null, outputDir = null, format, complete) {
    super(versionId, locale, version, project, creator, outputDir, format, complete);
    this.baseDir = __dirname;
    this.statistics = {
      testCount: 0,
      passedCount: 0,
      failedCount: 0,
      line: 0,
      commentLine: 0,
      functionLine: 0,
      commentCoverageMax: 0,
      commentCoverageMin: 0,
      commentCoverageLt20: 0,
      functionCount: 0,
      fileCount: 0,
      cycleComplexityMax: 0,
      cycleComplexityTotal: 0,
      cycleComplexityGt10: 0,
      cycleComplexityGt20: 0,
      cycleComplexityGt40: 0,
      functionLineGt200: 0,
      functionLineMax: 0,
      headerFileCount: 0,
      sourceFileCount: 0,
      funcs: [],
    };
  }

  getCoverage(a, b) {
    if (b) {
      return `${Math.round((a / b) * 10000) / 100}%`;
    }
    return '0%';
  }

  async generate(progressIndicator) {
    const fileName = '软件单元测试报告';
    const result = {
      cachePath: this.outputDir,
    };
    const { versionPath, projectDir } = await this.spliteByProjectVersion();
    this.versionDir = versionPath;
    this.projectDir = projectDir;
    const reportPath = `${path.join(this.versionDir, fileName)}`;
    await this.spliteByFile(progressIndicator);
    this.statistics.commentCoverage = this.getCoverage(this.statistics.commentLine, this.statistics.line);
    this.statistics.commentCoverageLt20Coverage = this.getCoverage(this.statistics.commentCoverageLt20, this.statistics.fileCount);
    this.statistics.commentCoverageMax = `${this.statistics.commentCoverageMax}%`;
    this.statistics.commentCoverageMin = `${this.statistics.commentCoverageMin}%`;
    this.statistics.cycleComplexityAvg = this.statistics.functionCount
      ? Math.round(this.statistics.cycleComplexityTotal / this.statistics.functionCount)
      : 0;
    this.statistics.functionLineAvg = this.statistics.functionCount ? Math.round(this.statistics.functionLine / this.statistics.functionCount) : 0;
    this.statistics.cycleComplexityGt10Covergae = this.getCoverage(this.statistics.cycleComplexityGt10, this.statistics.functionCount);
    this.statistics.cycleComplexityGt20Covergae = this.getCoverage(this.statistics.cycleComplexityGt20, this.statistics.functionCount);
    this.statistics.cycleComplexityGt40Covergae = this.getCoverage(this.statistics.cycleComplexityGt40, this.statistics.functionCount);
    this.statistics.functionLineGt200Coverage = this.getCoverage(this.statistics.functionLineGt200, this.statistics.functionCount);
    const versionOverview = await projectVersions.getOverview(this.versionId);
    const groupFuncs = {};
    this.statistics.funcs.forEach((f) => {
      if (groupFuncs[f.className] === undefined) {
        groupFuncs[f.className] = {
          name: f.className,
          subFuncs: [],
        };
      }
      f.idx = groupFuncs[f.className].subFuncs.length + 1;
      groupFuncs[f.className].subFuncs.push(f);
    }, []);
    await this.generateFileReport(
      {
        ...this.statistics,
        statementCoverage: this.getCoverage(versionOverview.statementCovered, versionOverview.statementTotal),
        branchCoverage: this.getCoverage(versionOverview.branchCovered, versionOverview.branchTotal),
        mcdcCoverage: this.getCoverage(versionOverview.mcdcCovered, versionOverview.mcdcTotal),
        projectName: this.project.projectName,
        versionName: this.version.versionName,
        funcs: this.statistics.funcs.map((f, idx) => {
          f.idx = idx + 1;
          return f;
        }),
        groupFuncs: Object.values(groupFuncs),
        reportDate: moment().format('YYYYMMDD'),
      },
      reportPath
    );
    result.reportPath = path.join(this.outputDir, `${fileName}.zip`);
    const reportsDir = path.join(this.outputDir, this.project.projectName);
    await compressing.zip.compressDir(reportsDir, result.reportPath);
    return result;
  }

  async spliteByFile(progressIndicator) {
    await fs.ensureDir(this.versionDir);
    const sourceFileExts = [];
    const languages = { ...this.version.languages, headers: this.version.headers };
    Object.keys(languages).forEach((key) => {
      if (languages[key].suffixes) {
        sourceFileExts.push(...languages[key].suffixes);
      }
    });

    const resFiles = new Map();
    const filter = {
      versionId: this.version._id,
      kind: files.kind.file,
    };
    if (sourceFileExts && sourceFileExts.length > 0) {
      filter.extname = { $in: sourceFileExts };
    }
    const allFiles = await File.find(filter, ['_id', 'fileName', 'path', 'numberOfLines', 'commentLines']).lean();
    allFiles.forEach((file) => {
      resFiles.set(file._id.toString(), file);
    });
    return this.handleFiles([...resFiles.values()], this.versionDir, progressIndicator);
  }

  async handleFiles(files, versionDir, progressIndicator) {
    return utility.arrayChunkOperation(files, 1, async (file) => {
      const result = await this.handleFile(file, versionDir);
      if (progressIndicator) {
        progressIndicator.advance(1 / files.length);
      }
      return result;
    });
  }

  async handleFile(file, versionDir) {
    this.statistics.fileCount += 1;
    this.statistics.line += file.numberOfLines;
    this.statistics.commentLine += file.commentLines;
    const headerExts = new Set([this.version.headers.suffixes]);
    if (headerExts.has(file.extname)) {
      this.statistics.headerFileCount += 1;
    } else {
      this.statistics.sourceFileCount += 1;
    }
    if (file.commentLines / file.numberOfLines < 0.2) {
      this.statistics.commentCoverageLt20 += 1;
    }
    let commentCoverage = 0;
    if (file.numberOfLines) {
      commentCoverage = Math.round((file.commentLines / file.numberOfLines) * 10000) / 100;
    }
    this.statistics.commentCoverageMax = Math.max(
      this.statistics.commentCoverageMax,
      commentCoverage
    );
    this.statistics.commentCoverageMin = Math.min(
      this.statistics.commentCoverageMin,
      commentCoverage
    );
    const res = await ProjectVersionSpliter.prototype.handleFile.call(this, file, versionDir);
    const { typeSystem, functions } = res;
    ParentReporter.getSummaryOfFile(functions);
    return utility.arrayChunkOperation(
      functions.filter((func) => fileElements.isTestableElement(func.kind)),
      1,
      async (func) => {
        this.statistics.cycleComplexityTotal += func.cycleComplexity;
        this.statistics.cycleComplexityMax = Math.max(this.statistics.cycleComplexityMax, func.cycleComplexity);
        if (func.cycleComplexity > 10) {
          this.statistics.cycleComplexityGt10 += 1;
        } else if (func.cycleComplexity > 20) {
          this.statistics.cycleComplexityGt20 += 1;
        } else if (func.cyclyComplexity > 40) {
          this.statistics.cycleComplexityGt40 += 1;
        }
        this.statistics.functionLine += func.numberOfLines;
        this.statistics.functionLineMax = Math.max(this.statistics.functionLineMax, func.numberOfLines);
        if (func.numberOfLines > 200) {
          this.statistics.functionLineGt200 += 1;
        }
        if ([...func.parents].length > 0) {
          const parents = await Func.find({ versionId: this.version._id, _id: { $in: [...func.parents] } });
          parents.forEach((parent) => {
            if (parent.kind === fileElements.elementKind.class || parent.kind === fileElements.elementKind.specialClass) {
              func.className = parent.functionName;
            }
          });
        }
        const functionCfg = await funcs.getFunctionCFG(func._id);
        const testInfoMap = {};
        func.tests.forEach((test) => {
          testInfoMap[test._id.toString()] = {
            log: test.log,
          };
        });
        let tests = this.getTestDetailsOfFunction(typeSystem, func);
        tests = tests.filter((test) => test.resultKind === 'succeed'
            || test.resultKind === 'nonStrictSucceed'
            || test.resultKind === 'noResult'
            || test.resultKind === 'failed');
        tests.forEach((test, index) => {
          let branchExpect = this.getTestExpectedResultDetails(test.testId.toString(), {
            branches: functionCfg.branches,
            decisions: functionCfg.decisions,
            nodes: functionCfg.nodes,
          });
          if (branchExpect.length === 0) {
            branchExpect = `进入${func.functionName}`;
          } else {
            branchExpect = branchExpect.join(';');
          }
          test.idx = index + 1;
          const newInputDetails = [];
          const newOutputDetails = [];
          this.statistics.testCount += 1;
          if (test.resultKind === 'failed') {
            this.statistics.failedCount += 1;
          } else {
            this.statistics.passedCount += 1;
          }
          if (utility.isArrayType(test.detail.input)) {
            test.detail.input.forEach((input) => {
              // eslint-disable-next-line no-control-regex
              input.variable = input.variable.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
              // 处理变量值显示格式
              if (input.value !== '') {
                input.value = ` = ${input.value}`;
              }
              if (input.category && input.variable) {
                newInputDetails.push({
                  category: `${input.category}:`,
                  value: '',
                  variable: '',
                });
                newInputDetails.push({
                  category: '',
                  value: input.value,
                  variable: input.variable,
                });
              } else {
                newInputDetails.push(input);
              }
            });
          }
          if (utility.isArrayType(test.detail.output)) {
            test.detail.output.forEach((output) => {
              // eslint-disable-next-line no-control-regex
              output.variable = output.variable.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
              // eslint-disable-next-line no-nested-ternary
              output.value = output.value !== ''
                ? ` = ${output.value}`
                : (output.result !== '' ? ` = ${output.result}` : '');
              // 处理变量值显示格式
              if (output.result !== '') {
                output.result = ` = ${output.result}`;
              }
              if (output.category && output.variable) {
                newOutputDetails.push({
                  category: `${output.category}:`,
                  value: '',
                  result: '',
                  variable: '',
                });
                newOutputDetails.push({
                  category: '',
                  value: output.value,
                  result: output.result,
                  variable: output.variable,
                });
              } else {
                newOutputDetails.push(output);
              }
            });
          }
          test.detail.input = newInputDetails;
          test.detail.output = newOutputDetails;
          test.branchExpect = branchExpect;
          test.functionName = func.functionName;
        });
        this.statistics.funcs.push(
          {
            functionName: func.functionName,
            className: func.className || file.fileName,
            fileName: file.fileName,
            tests,
          }
        );
        this.statistics.functionCount += 1;
      }
    );
  }

  generateFileReport(data, output) {
    const template = 'file-template';
    data.reportFooterText = this.reportFooterText;
    return (async () => {
      output = `${output}.docx`;
      const tpl = new Docx(path.join(this.baseDir, 'template', 'docx', `${template}.docx`));
      await tpl.generate(
        data,
        output,
        true
      );
      return output;
    })();
  }

  getTestExpectedResultDetails(testcaseId, functionCFG) {
    const { branches, decisions, nodes } = functionCFG;
    if (branches
      && Object.keys(branches).length > 0
      && nodes
      && Object.keys(nodes).length > 0) {
      const branchExprs = [];
      Object.keys(branches).forEach((branchKey) => {
        const branch = branches[branchKey];
        let expr = '';
        if (branch.decisionId !== undefined && decisions[branch.decisionId]) {
          expr = decisions[branch.decisionId].expr;
        }
        Object.keys(branch.branches).forEach((nodeId) => {
          const branchNode = branch.branches[nodeId];
          if (branchNode
            && branchNode.testIds
            && branchNode.testIds[testcaseId]
            && branchNode.testIds[testcaseId] > 0) {
            branchExprs.push(`进入${expr}分支${branchNode.kind === 'BOOLEAN' ? branchNode.value === '1' : branchNode.value}`);
          }
        });
      });
      return branchExprs;
    }
    return [];
  }
}

/**
 * Export model definition object.
 */
module.exports = DefaultReporter;
